<html>
<head>
  <title>18.备忘录（Memento）模式</title>
  <basefont face="微软雅黑" size="2" />
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name="exporter-version" content="YXBJ Windows/602628 (zh-CN, DDL); Windows/10.0.0 (Win64); EDAMVersion=V2;"/>
  <style>
    body, td {
      font-family: 微软雅黑;
      font-size: 10pt;
    }
  </style>
</head>
<body>
<a name="25493"/>
<h1>18.备忘录（Memento）模式</h1>
<div>
<table bgcolor="#D4DDE5" border="0">
<tr><td><b>创建时间：</b></td><td><i>2020/9/13 8:15</i></td></tr>
<tr><td><b>更新时间：</b></td><td><i>2021/3/11 0:54</i></td></tr>
<tr><td><b>作者：</b></td><td><i>Long Life Student</i></td></tr>
</table>
</div>
<br/>

<div>
<span><div><div><span style="font-weight: bold;">一、基本描述</span></div><ul><li><div>在不破坏封装性的前提下，捕获一个对象的内部状态，并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。</div></li><li><div>别名：Token</div></li><li><div>有时有必要记录一个对象的内部状态，为了允许用户取消不确定的操作或从错误中恢复过来，需要实现检查点和取消机制，而要实现这些机制，必须实现将状态保存在某处，这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息，使得其状态不能被其他对象访问，也就不可能在该对象之外保存其状态。而暴露其内部状态又将违反封装的原则，可能有损应用的可靠性和可扩展性。</div></li><li><div>可以使用备忘录模式解决这一问题。一个备忘录是一个对象，它存储另一个对象在某个瞬间的内部状态，而后者称为备忘录的原发器。当需要设置原发器的检查点时，取消操作机制会向原发器请求一个备忘录。原发器用描述当前状态的信息初始化该备忘录。只有原发器可以向备忘录中存取信息，备忘录对其他的对象“不可见”。</div></li><li><div>在以下情况下使用备忘录模式：</div></li></ul><div style="padding-left: 30px;">1）必须保存一个对象在某个时刻的（部分）状态，这样以后需要时它才能恢复到先前的状态。</div><div style="padding-left: 30px;">2）如果一个接口让其他对象直接得到这些状态，将会暴露对象的实现细节并破坏对象的封装性。</div><div style="padding-left: 30px;">3） 数据库连接的事务管理就是用的备忘录模式，如JDBC驱动使用备忘录模式来实现事务。</div><ul><li><div>备忘录模式比较适用于功能比较复杂的，但需要维护或记录属性历史的类，或者需要保存的属性只是众多属性中的一小部分时，Originator可以根据保存的Memento信息还原前一状态。如果在某个系统中使用命令模式时，需要实现命令的撤销功能，那么命令模式可以使用备忘录模式来存储可撤销操作的状态，有时一些对象的内部信息必须保存在对象以外的地方，但必须要由对象自己读取，这时，使用备忘录可以把复杂的对象内部信息对其他的对象屏蔽起来，从而可以恰当的保持封装的边界。</div></li><li><div>备忘录模式最大的作用还是在当角色的状态改变的时候，有可能这个状态无效，此时就可以使用暂时存储起来的备忘录将状态复原这个作用。</div></li><li><div>通俗地说， 备忘录模式就是一个对象的备份模式， 提供了一种程序数据的备份方法。</div></li><li><div>由于备忘录模式有太多的变形和处理方式， 每种方式都有它自己的优点和缺点， 标准的<span style="font-size: unset; color: unset; font-family: unset;">备忘录模式很难在项目中遇到， 基本上都有一些变换处理方式。</span></div></li><li><div>不要在频繁建立备份的场景中使用备忘录模式， 一是<span style="font-size: unset; color: unset; font-family: unset;">控制不了备忘录建立的对象数量； 二是大对象的建立是要消耗资源的， 系统的性能需要考</span><span style="font-size: unset; color: unset; font-family: unset;">虑。</span></div></li><li><div>通过使用备忘录可以使系统恢复到某一个特定的历史状态。</div></li><li><div>备忘录模式提供了一种状态恢复的实现机制， 使得用户可以方便地回到一个特定的历史步<span style="font-size: unset; color: unset; font-family: unset;">骤， 当新的状态无效或者存在问题时， 可以使用暂时存储起来的备忘录将状态复原。</span></div></li><li><div>备忘录模式的核心是备忘录类（Memento）以及用于管理备忘录的管理者类的（Caretaker）设计。</div></li><li><div>备忘录模式在很多软件的使用过程中普遍存在， 但是在应用软件开发中， 它的使用频率并不<span style="font-size: unset; color: unset; font-family: unset;">太高， 因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。 如果需要为软件提</span><span style="font-size: unset; color: unset; font-family: unset;">供撤销功能， 备忘录模式无疑是一种很好的解决方案。 在一些字处理软件、 图像编辑软件、</span><span style="font-size: unset; color: unset; font-family: unset;">数据库管理系统等软件中备忘录模式都得到了很好的应用。</span></div></li><li><div>当你需要让对象返回之前的状态时，就使用备忘录模式。</div></li><li><div>备忘录模式有两个目标：</div></li><ul><li><div>储存系统关键对象的重要状态</div></li><li><div>维护关键对象的封装。</div></li></ul></ul><div style="margin-left: 40px;">单一责任原则要求我们不要把保持状态的工作和关键对象混在一起，这个专门掌握状态的对象，就称为备忘录。<br/></div><div><span style="font-weight: bold;">二、图例</span></div><div><img src="18.备忘录（Memento）模式_files/截图录屏_选择区域_20200914103954.png" type="image/png" data-filename="截图录屏_选择区域_20200914103954.png"/></div><ul><li><div><span style="font-weight: bold;">Memento（备忘录）：</span>备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些内部状态。防止原发器以外的其他对象访问备忘录。备忘录实际上有两个接口，管理者（caretaker）只能看到备忘录的窄接口——它只能将备忘录传递给其他对象。相反，原发器能够看到一个宽接口，允许它访问返回到先前状态所需的所有数据。理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的内部状态。主要是在需要的时候提供发起人需要的内部状态。通常提供了与原发器相对应的属性（可以是全部，也可以是部分），用于存储原发器的状态。</div></li><li><div><span style="font-weight: bold;">Originator（原发器）：</span>原发器创建一个备忘录，用以记录当前时刻它的内部状态。使用备忘录恢复内部状态。负责定义哪些属于备份范围的状态。一般将需要保存内部状态的类设计为原发器。</div></li></ul><div style="padding-left: 30px;">原发器类中有三个成员：</div><div style="padding-left: 30px;">1. SetMemento(Memento m)：恢复备忘录，将Memento导入并将相关的数据恢复。</div><div style="padding-left: 30px;">2. CreateMemento()：创建备忘录，将当前需要保存的信息导入并实例化出一个Memento对象。</div><div style="padding-left: 30px;">3. state：需要保存的属性，可能有多个。</div><ul><li><div><span style="font-weight: bold;">Caretaker（管理者）：</span>负责保存好备忘录。不能对备忘录的内容进行操作或检查。对备忘录进行管理，保存和提供备忘录，只负责存储备忘录的对象，但不能对对象进行修改，也无需知道细节。在负责人类中，不应直接调用Memento中的状态改变方法，它的作用仅仅用于存储备忘录对象，将原发器备份生成的备忘录对象存储在其中，当用户需要恢复时，再将存储的备忘录对象取出。=&gt;只保存备忘录对象，不做其他的事。</div></li><li><div><span style="font-weight: bold;">协作：</span>管理者向原发器请求一个备忘录，保留一段时间后，将其送回给原发器，有时管理者不会将备忘录返回给原发器。因为原发器可能根本不需要退到先前的状态。备忘录是被动的，只有创建备忘录的原发器会对它的状态进行赋值和检索。</div></li></ul><div><span style="font-weight: bold;">三、优缺点</span></div><div><span style="font-weight: bold;">优点</span></div><ul><li><div><span style="font-weight: bold;">保持封装边界。</span>使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。该模式把可能很复杂的Originator内部信息对其他对象屏蔽起来，从而保持了封装边界。</div></li><li><div><span style="font-weight: bold;">简化了原发器。</span>在其他的保持封装性的设计中，Originator负责保持客户请求过的内部状态版本。这就把所有存储管理的重任交给了Originator。让客户管理请求的状态将会简化Originator，并且使得客户工作结束时无需通知原发器。</div></li><li><div>将被储存的状态放在外面，不要和关键对象混在一起，这可以帮助维护内聚。</div></li><li><div>保持关键对象的数据封装。</div></li><li><div>提供了容易实现的恢复能力。</div></li></ul><div><span style="font-weight: bold;">缺点</span></div><ul><li><div><span style="font-weight: bold;">使用备忘录可能代价很高。</span>如果原发器在生成备忘录时必须拷贝并存储大量的信息，或者客户非常频繁的创建备忘录和恢复原发器状态，可能会导致非常大的开销。除非封装和恢复Originator状态的开销不大，否则该模式可能并不合适。备忘录对象会非常消耗内存。</div></li><li><div><span style="font-weight: bold;">定义窄接口和宽接口。</span>在一些语言中可能难以保证只有原发器可访问备忘录的状态。</div></li><li><div><span style="font-weight: bold;">维护备忘录的潜在代价。</span>管理者负责删除它所维护的备忘录。然而，管理者不知道备忘录中有多少个状态，因此当存储备忘录时，一个本来很小的管理者可能会产生大量的存储开销。</div></li></ul><div><span style="font-weight: bold;">四、可能的改进</span></div><ul><li><div><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test/tree/master/Java/src/com/company/memento/section6">使用Clone方式（原型模式）实现备忘录。</a></div></li><ul><li><div><span style="font-size: unset; color: unset; font-family: unset;">通过复制的方式产生一个对象的内部状态的备份。</span></div></li><li><div>发起人角色融合了发起人角色和备忘录角色，甚至可以融合管理者。</div></li><li><div>原型模式存在深拷贝和浅拷贝的问题， 在复杂的场景下它会 让你的程序逻辑异常混乱， 出现错误也很难跟踪。 因此Clone方式的备忘录模式适用于较简<span style="font-size: unset; color: unset; font-family: unset;">单的场景。</span></div></li><li><div>使用Clone方式的备忘录模式， 可以使用在比较简单的场景或者比较单一的场景<span style="font-size: unset; color: unset; font-family: unset;">中， 尽量不要与其他的对象产生严重的耦合关系。</span></div></li></ul><li><div><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test/tree/master/Java/src/com/company/memento/section7">多状态的备忘录模式。</a></div></li><ul><li><div>备忘录模式的扩展，实现对象所有状态的备份与还原。</div></li><li><div>如果要设计一个在运行期决定备份状态的框架， 则建议采用AOP框架来实现， 避<span style="font-size: unset; color: unset; font-family: unset;">免采用动态代理无谓地增加程序逻辑复杂性。</span></div></li></ul><li><div><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test/tree/master/Java/src/com/company/memento/section8">多备份的备忘录</a></div></li><ul><li><div>在管理员类中，将容纳备忘录的容器修改为Map类型即可。</div></li><li><div><span style="font-size: unset; color: unset; font-family: unset;">在系统设计时， 要严格限定备忘录的创建， 建议增加Map的上限， 否则系统很</span><span style="font-size: unset; color: unset; font-family: unset;">容易产生内存溢出情况。</span></div></li><li><div>可实现多次撤销：可对Map进行逆向遍历，回到一个指定的历史状态，还可以对集合进行正向遍历，实现重做操作。</div></li></ul><li><div>更好封装的备忘录（实现宽接口与窄接口的备忘录）</div></li><ul><li><div>双接口设计： 考虑对象的安全问题， 可以提供两个接口， 一个是业务的正常接口，<span style="font-size: unset; color: unset; font-family: unset;">实现必要的业务逻辑， 叫做宽接口； 另外一个接口是一个空接口， 什么方法都没有， 其目的</span><span style="font-size: unset; color: unset; font-family: unset;">是提供给子系统外的模块访问，叫做窄接口， 由于窄接口中没有提供任</span><span style="font-size: unset; color: unset; font-family: unset;">何操纵数据的方法， 因此相对来说比较安全。</span></div></li><li><div><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test/tree/master/Java/src/com/company/memento/section9">Java中，使用内置类实现备忘录</a>：在<span style="font-size: unset; color: unset; font-family: unset;">原发器</span>类中建立一个内置类备忘录。</div></li><li><div>Java中，可以 通过将Memento类与Originator类定义在同一个包<span style="font-size: unset; color: unset; font-family: unset;">(package)中来实现封装：</span> 在Java语言中可使用默认访问标识符来定义Memento类， 即保证其包<span style="font-size: unset; color: unset; font-family: unset;">内可见。 将原发器和备忘录类放到同一个包中，让它们之间满足默认的包内可见性。</span></div></li><li><div><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test/tree/master/Cplus/Memento/Memento_1">C++中，使用友元实现备忘录，让原发器类和备忘录类成为友元类</a>：Memento类中所有成员都做成私有的（private），Originator类作为Memento类的友元被声明。</div></li><li><div>在Java系统中，其实可以考虑使用序列化<span style="color: unset; font-family: unset; font-size: unset;">（ serialization）机制储存系统的状态。</span></div></li></ul></ul><div><br/></div><div><span style="font-weight: bold;">五、个人理解</span></div><ul><li><div>备忘录模式在理解与实现上并不复杂。</div></li><li><div>备忘录的实现与使用的语言有关。</div></li><li><div>备忘录的应用场景明确，功能比较专一，在应用时有相关场景很容易想到备忘录。</div></li></ul><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div></span>
</div></body></html> 